Lab 9: Generative Art

Author

Jacob Perez

Libraries

# load necessary libraries
library(dplyr)
library(purrr)
library(tidyr)
library(tibble)
library(ggplot2)
library(ambient)
library(tictoc)
library(ggthemes)
library(gifski)
library(scales)

For Art Piece 1

# defines two R functions: `sample_data` for generating random data and `polar_styled_plot` for creating a ggplot2 visualization.

# Function 1: sample_data
# Generates a tibble (a modern data frame) with random numeric and factor data,
sample_data <- function(seed = NULL, n = 100){

  if(!is.null(seed)) set.seed(seed)

  # Create a tibble (data frame) with 'n' rows.
  dat <- tibble(
    x0 = runif(n),
    y0 = runif(n),
    x1 = x0 + runif(n, min = -.2, max = .2),
    y1 = y0 + runif(n, min = -.2, max = .2),
    
    shade = runif(n),

    size = runif(n),

    shape = factor(sample(0:22, size = n, replace = TRUE))
  )
  # The function returns the created 'dat' tibble.
}

# Function 2: polar_styled_plot
# Creates a ggplot2 object designed for a specific "Perlin noise" aesthetic
polar_styled_plot <- function(data = NULL, palette) {
  
  ggplot(
    data = data,
    mapping = aes(
      x = x0,
      y = y0,
      xend = x1,
      yend = y1,
      colour = shade,
      size = size
    )) +
    # Transforms the Cartesian coordinate system into a polar coordinate system.
    coord_polar(clip = "off") +
    # `oob = scales::oob_keep`: Instructs ggplot to keep out-of-bounds data when transforming coordinates
    scale_y_continuous(
      expand = c(0, 0),
      limits = c(0, 1),
      oob = scales::oob_keep
    ) +
    # Customizes the x-axis angular axis in polar coordinates
    scale_x_continuous(
      expand = c(0, 0),
      limits = c(0, 1),
      oob = scales::oob_keep
    ) +
    # Defines a continuous color scale using a custom gradient.
    scale_colour_gradientn(colours = palette) +
    # Defines a continuous size scale, mapping data 'size' values to a visual
    scale_size(range = c(0, 10)) +
    # Uses a minimalist theme that removes all plot background, axis lines, and labels.
    theme_void() +

    guides(
      colour = guide_none(),
      size = guide_none(),
      fill = guide_none(),
      shape = guide_none()
    )
  # The function returns the created ggplot object.
}

For Art Piece 2

# Function to visualize a polygon using ggplot2.
show_polygon <- function(polygon, show_vertices = TRUE, ...) {

  # Create a ggplot object from the 'polygon' data 
  pic <- ggplot(polygon, aes(x, y)) +
    geom_polygon(colour = "black", fill = NA, show.legend = FALSE, ...) +
    coord_equal() +
    theme_void()

  # Optionally add points to show the vertices of the polygon.
  if(show_vertices == TRUE) {
    pic <- pic + geom_point(colour = "black", size = 2)
  }
  return(pic)
}

# Function to calculate the x-coordinate for an hourglass shape.
hourglass_x <- function(angle) {
  # Formula I played with until it looked right.
  x <- sin(angle) * abs(cos(angle))^1.2
  return(x)
}

# Function to calculate the y-coordinate for an hourglass shape.
hourglass_y <- function(angle) {
  y <- cos(angle)
  return(y)
}

# Creates a tibble representing the base hourglass shape.
hourglass_shape <- tibble(
  angle = seq(0, 2 * pi, length.out = 50),
  x = hourglass_x(angle),
  y = hourglass_y(angle)
)

# Normalizes a numeric vector to a specified range (min, max).
normalize_radius <- function(x, min, max) {
  scales::rescale(x, from = c(-0.5, 0.5), to = c(min, max))
}

# Generates data for a single "Perlin noise" hourglass.
perlin_hourglass2 <- function(n = 100,
                              freq_init = 0.3,
                              octaves = 2,
                              r_min = 0.5,
                              r_max = 1,
                              w_min = 0,
                              w_max = 4,
                              rot = 0,
                              x_shift = 0,
                              y_shift = 0,
                              id = NA,
                              seed = NULL) {

  if(!is.null(seed)) set.seed(seed)
  tibble(
    angle = seq(0, 2*pi, length.out = n),
    
    radius = fracture(
      x = cos(angle),
      y = sin(angle),
      freq_init = freq_init,
      noise = gen_perlin,
      fractal = fbm,
      octaves = octaves
    ) |>
      normalize_radius(r_min, r_max),

    # Calculates final x and y coordinates based on radius, hourglass shape, and shifts.
    x = radius * hourglass_x(angle) + x_shift,
    y = radius * hourglass_y(angle) + y_shift,

    width = fracture(
      x = cos(angle + rot),
      y = sin(angle + rot),
      freq_init = freq_init,
      noise = gen_perlin,
      fractal = fbm,
      octaves = octaves
    ) |>
      normalize(to = c(w_min, w_max)),

    id = id
  )
}

# Generates data for multiple Perlin hourglasses with scatter and color.
perlin_hourglass_data_2 <- function(nhourglasses = 10, scatter = 0.05, palette = NULL) {

  # Checks if the provided palette has enough colors for all hourglasses.
  if (length(palette) < nhourglasses) {
    stop("Palette must have at least as many colors as nhourglasses")
  }

  hourglass_settings <- tibble(
    id = 1:nhourglasses,
    n = 500,
    r_min = 0.35,
    r_max = 0.4,
    w_min = -10,
    w_max = 10,

    x_shift = runif(nhourglasses, -scatter / 2, scatter / 2),
    y_shift = runif(nhourglasses, -scatter / 2, scatter / 2),
    rot = runif(nhourglasses, -pi, pi)
  )

  hourglass_settings |>
    pmap_dfr(perlin_hourglass2) |>
    group_by(id) |>
    mutate(
      shade = palette[id], # Assigns a color from the palette based on ID.
      width = abs(width)
    )
}

# Generates a single ggplot frame (image) of the hourglasses.
generate_one_frame <- function(dat) {

  pic <- dat |>
    ggplot(aes(x, y, group = id, size = width, colour = shade)) +
    geom_path(show.legend = FALSE) +
    theme_void() +
    scale_x_continuous(expand = c(0, 0)) +
    scale_y_continuous(expand = c(0, 0)) +
    scale_colour_identity() +
    scale_size_identity() +
    coord_fixed(xlim = c(-0.6, 0.6), ylim = c(-0.6, 0.6))

  print(pic)
}

rotate_vector <- function(x, percent) {

  len <- length(x) 
  ind <- ceiling(len * percent) 
  if (ind == 0) return(x) 
  if (ind == len) return(x) 
  c(x[(ind + 1):len], x[1:ind])
}

# Generates all frames for an animation.
generate_all_frames <- function(dat, nframes = 100) {

  for (frame in 1:nframes) {
    dat_frame <- dat |>
      group_by(id) |>
      mutate(width = rotate_vector(width, frame / nframes))

    # Generates and prints the plot for the current frame.
    generate_one_frame(dat_frame)
  }
}

# Main function to create and save the animated Perlin hourglass GIF.
animated_perlin_hourglass_2 <- function(palette_name, palette, ...) {

  save_gif(
    expr = perlin_hourglass_data_2(palette = palette, ...) |> generate_all_frames(),
    # creates the output GIF filename
    gif_file = paste0("animated-perlin-hourglass-", palette_name, ".gif"),
    height = 1000, 
    width = 1000,  
    delay = 0.1,   
    progress = TRUE, 
    bg = "#222222" 
  )
  invisible(NULL)
}
# creates the desired color palettes for my hourglasses

sandstorm_ekko <- c("#733726", "#3d2934", "#a87750", "#f3e791", "#d4b677", "#cebb92", "#f0ebd2", "#73564a", "#e38d4a", "#a75919", "#322755", "#a53a65")

pulsefire_ekko <- c("#dbcdc9", "#2b3247", "#576b95", "#76bae1", "#5b89c1", "#7f6565", "#545061", "#202531", "#e6bc92", "#606c7a", "#a46360", "white")

starguardian_ekko <- c("#e4a6b3", "#3e2848", "#8e74c7", "#9d5f7e", "#6c5294", "#764761", "#aaade8", "white", "#cd464c", "#7c6d68", "#f8552a", "#e0ab58")